﻿/* System.Compute: a Computation Framework for .NET/Mono
 * Copyright (C) 2011 Ralf "raboni" Boeckhorst
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 3 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General
 * Public License along with this library; if not, write to the
 * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 * Boston, MA 02110-1301 USA
 */

using System.Runtime.InteropServices;
using System.Text;

namespace System.Compute.OpenCL
{
    public sealed class Program : ICloneable, IDisposable
    {
        delegate void pfnNotify(string errinfo, byte[] private_info, SizeT cb, byte[] user_data);

        [DllImport("opencl.dll", ExactSpelling = true)]
        static extern IntPtr clCreateProgramWithSource(IntPtr context, uint count, string[] strings, SizeT[] lengths, ref ErrorCode errcode_ret);

        [DllImport("opencl.dll", ExactSpelling = true)]
        static extern IntPtr clCreateProgramWithBinary(IntPtr context, uint num_devices, IntPtr[] device_list, SizeT[] lengths, IntPtr[] binaries, IntPtr binary_status, ref ErrorCode errcode_ret);

        [DllImport("opencl.dll", ExactSpelling = true)]
        static extern IntPtr clCreateProgramWithBuildInKernels(IntPtr context, uint num_devices, IntPtr[] device_list, string kernel_names, ref ErrorCode errcode_ret);

        [DllImport("opencl.dll", ExactSpelling = true)]
        static extern ErrorCode clGetProgramInfo(IntPtr program, ProgramInfo param_name, SizeT param_value_size, IntPtr param_value, ref SizeT param_value_size_ret);

        [DllImport("opencl.dll", ExactSpelling = true)]
        static extern ErrorCode clGetProgramInfo(IntPtr program, ProgramInfo param_name, SizeT param_value_size, ref uint param_value, SizeT param_value_size_ret);

        [DllImport("opencl.dll", ExactSpelling = true)]
        static extern ErrorCode clGetProgramInfo(IntPtr program, ProgramInfo param_name, SizeT param_value_size, ref IntPtr param_value, SizeT param_value_size_ret);

        [DllImport("opencl.dll", ExactSpelling = true)]
        static extern ErrorCode clGetProgramInfo(IntPtr program, ProgramInfo param_name, SizeT param_value_size, IntPtr[] param_value, SizeT param_value_size_ret);

        [DllImport("opencl.dll", ExactSpelling = true)]
        static extern ErrorCode clGetProgramInfo(IntPtr program, ProgramInfo param_name, SizeT param_value_size, byte[] param_value, SizeT param_value_size_ret);

        [DllImport("opencl.dll", ExactSpelling = true)]
        static extern ErrorCode clBuildProgram(IntPtr program, uint num_devices, IntPtr[] device_list, string options, pfnNotify notify, byte[] user_data);

        [DllImport("opencl.dll", ExactSpelling = true)]
        static extern ErrorCode clGetProgramBuildInfo(IntPtr program, IntPtr device, ProgramBuildInfo param_name, SizeT param_value_size, IntPtr param_value, ref SizeT param_value_size_ret);

        [DllImport("opencl.dll", ExactSpelling = true)]
        static extern ErrorCode clGetProgramBuildInfo(IntPtr program, IntPtr device, ProgramBuildInfo param_name, SizeT param_value_size, ref BuildStatus param_value, SizeT param_value_size_ret);

        [DllImport("opencl.dll", ExactSpelling = true)]
        static extern ErrorCode clGetProgramBuildInfo(IntPtr program, IntPtr device, ProgramBuildInfo param_name, SizeT param_value_size, byte[] param_value, SizeT param_value_size_ret);

        [DllImport("opencl.dll", ExactSpelling = true)]
        static extern ErrorCode clGetProgramBuildInfo(IntPtr program, IntPtr device, ProgramBuildInfo param_name, SizeT param_value_size, ref ProgramBinaryType param_value, SizeT param_value_size_ret);

        [DllImport("opencl.dll", ExactSpelling = true)]
        static extern ErrorCode clUnloadCompiler();

        [DllImport("opencl.dll", ExactSpelling = true)]
        static extern ErrorCode clUnloadPlatformCompiler();

        [DllImport("opencl.dll", ExactSpelling = true)]
        static extern ErrorCode clRetainProgram(IntPtr program);

        [DllImport("opencl.dll", ExactSpelling = true)]
        static extern ErrorCode clReleaseProgram(IntPtr program);

        public uint ReferenceCount
        {
            get
            {
                uint ret = 0;
                ErrorCode res = clGetProgramInfo(Handle, ProgramInfo.ReferenceCount, sizeof(uint), ref ret, 0);
                if (res != ErrorCode.Success)
                    throw new OpenCLException(res);
                return ret;
            }
        }

        public Context Context
        {
            get
            {
                IntPtr ret = IntPtr.Zero;
                ErrorCode res = clGetProgramInfo(Handle, ProgramInfo.Context, IntPtr.Size, ref ret, 0);
                if (res != ErrorCode.Success)
                    throw new OpenCLException(res);
                return new Context(ret);
            }
        }

        public uint NumDevices
        {
            get
            {
                uint ret = 0;
                ErrorCode res = clGetProgramInfo(Handle, ProgramInfo.NumDevices, sizeof(uint), ref ret, 0);
                if (res != ErrorCode.Success)
                    throw new OpenCLException(res);
                return ret;
            }
        }

        public Device[] Devices
        {
            get
            {
                uint num_devices = NumDevices;
                IntPtr[] ret = new IntPtr[num_devices];
                ErrorCode res = clGetProgramInfo(Handle, ProgramInfo.Devices, IntPtr.Size * num_devices, ret, 0);
                if (res != ErrorCode.Success)
                    throw new OpenCLException(res);
                Device[] devices = new Device[num_devices];
                for (long i = 0; i < num_devices; i++)
                    devices[i] = new Device(ret[i]);
                return devices;
            }
        }

        public string Source
        {
            get
            {
                SizeT param_value_size = 0;
                ErrorCode res = clGetProgramInfo(Handle, ProgramInfo.Source, 0, IntPtr.Zero, ref param_value_size);
                if (res != ErrorCode.Success)
                    throw new OpenCLException(res);
                byte[] data = new byte[(long)param_value_size];
                res = clGetProgramInfo(Handle, ProgramInfo.Source, param_value_size, data, 0);
                if (res != ErrorCode.Success)
                    throw new OpenCLException(res);
                return Encoding.Default.GetString(data);
            }
        }

        public SizeT[] BinarySizes
        {
            get
            {
                SizeT param_value_size = 0;
                ErrorCode res = clGetProgramInfo(Handle, ProgramInfo.BinarySizes, 0, IntPtr.Zero, ref param_value_size);
                if (res != ErrorCode.Success)
                    throw new OpenCLException(res);
                IntPtr[] ret = new IntPtr[(long)param_value_size / (long)param_value_size.Size];
                res = clGetProgramInfo(Handle, ProgramInfo.BinarySizes, param_value_size, ret, 0);
                if (res != ErrorCode.Success)
                    throw new OpenCLException(res);
                SizeT[] tmp = new SizeT[ret.LongLength];
                for (long i = 0; i < ret.LongLength; i++)
                    tmp[i] = ret[i];
                return tmp;
            }
        }

        public byte[][] Binaries
        {
            get
            {
                SizeT[] sizes = BinarySizes;
                byte[][] ret = new byte[sizes.LongLength][];
                GCHandle[] handles = new GCHandle[sizes.LongLength];
                IntPtr[] binaries = new IntPtr[sizes.LongLength];
                for (long i = 0; i < sizes.LongLength; i++)
                {
                    ret[i] = new byte[sizes[i]];
                    handles[i] = GCHandle.Alloc(ret[i], GCHandleType.Pinned);
                    binaries[i] = handles[i].AddrOfPinnedObject();
                }
                ErrorCode res = clGetProgramInfo(Handle, ProgramInfo.Binaries, IntPtr.Size * sizes.LongLength, binaries, 0);
                if (res != ErrorCode.Success)
                    throw new OpenCLException(res);
                for (long i = 0; i < sizes.LongLength; i++)
                {
                    handles[i].Free();
                }
                return ret;
            }
        }

        public uint NumKernels
        {
            get
            {
                uint ret = 0;
                ErrorCode res = clGetProgramInfo(Handle, ProgramInfo.NumKernels, sizeof(uint), ref ret, 0);
                if (res != ErrorCode.Success)
                    throw new OpenCLException(res);
                return ret;
            }
        }

        public string KernelNames
        {
            get
            {
                SizeT param_value_size = 0;
                ErrorCode res = clGetProgramInfo(Handle, ProgramInfo.KernelNames, 0, IntPtr.Zero, ref param_value_size);
                if (res != ErrorCode.Success)
                    throw new OpenCLException(res);
                byte[] data = new byte[(long)param_value_size];
                res = clGetProgramInfo(Handle, ProgramInfo.KernelNames, param_value_size, data, 0);
                if (res != ErrorCode.Success)
                    throw new OpenCLException(res);
                return Encoding.Default.GetString(data);
            }
        }

        public BuildStatus[] BuildStatus
        {
            get
            {
                BuildStatus[] ret = new BuildStatus[NumDevices];
                Device[] devices = Devices;
                for (long i = 0; i < ret.LongLength; i++)
                {
                    ErrorCode res = clGetProgramBuildInfo(Handle, devices[i].Handle, ProgramBuildInfo.Status, sizeof(BuildStatus), ref ret[i], 0);
                    if (res != ErrorCode.Success)
                        throw new OpenCLException(res);
                }
                return ret;
            }
        }

        public string[] BuildOptions
        {
            get
            {
                string[] ret = new string[NumDevices];
                Device[] devices = Devices;
                for (long i = 0; i < ret.LongLength; i++)
                {
                    SizeT param_value_size = 0;
                    ErrorCode res = clGetProgramBuildInfo(Handle, devices[i].Handle, ProgramBuildInfo.Options, 0, IntPtr.Zero, ref param_value_size);
                    if (res != ErrorCode.Success)
                        throw new OpenCLException(res);
                    byte[] data = new byte[(long)param_value_size];
                    res = clGetProgramBuildInfo(Handle, devices[i].Handle, ProgramBuildInfo.Options, param_value_size, data, 0);
                    if (res != ErrorCode.Success)
                        throw new OpenCLException(res);
                    ret[i] = Encoding.Default.GetString(data);
                }
                return ret;
            }
        }

        public string[] BuildLog
        {
            get
            {
                string[] ret = new string[NumDevices];
                Device[] devices = Devices;
                for (long i = 0; i < ret.LongLength; i++)
                {
                    SizeT param_value_size = 0;
                    ErrorCode res = clGetProgramBuildInfo(Handle, devices[i].Handle, ProgramBuildInfo.Log, 0, IntPtr.Zero, ref param_value_size);
                    if (res != ErrorCode.Success)
                        throw new OpenCLException(res);
                    byte[] data = new byte[(long)param_value_size];
                    res = clGetProgramBuildInfo(Handle, devices[i].Handle, ProgramBuildInfo.Log, param_value_size, data, 0);
                    if (res != ErrorCode.Success)
                        throw new OpenCLException(res);
                    ret[i] = Encoding.Default.GetString(data);
                }
                return ret;
            }
        }

        public ProgramBinaryType[] BinaryType
        {
            get
            {
                ProgramBinaryType[] ret = new ProgramBinaryType[NumDevices];
                Device[] devices = Devices;
                for (long i = 0; i < ret.LongLength; i++)
                {
                    ErrorCode res = clGetProgramBuildInfo(Handle, devices[i].Handle, ProgramBuildInfo.BinaryType, sizeof(ProgramBinaryType), ref ret[i], 0);
                    if (res != ErrorCode.Success)
                        throw new OpenCLException(res);
                }
                return ret;
            }
        }

        internal IntPtr Handle;

        internal Program(IntPtr handle)
        {
            Handle = handle;
        }

        public Program(Context context, params string[] sources)
        {
            ErrorCode res = ErrorCode.Success;
            Handle = clCreateProgramWithSource(context.Handle, (uint)sources.Length, sources, null, ref res);
            if (res != ErrorCode.Success)
                throw new OpenCLException(res);
        }

        public Program(Context context, Device[] devices, byte[][] binaries)
        {
            ErrorCode res = ErrorCode.Success;
            IntPtr[] p_devices = new IntPtr[devices.LongLength];
            for (long i = 0; i < p_devices.LongLength; i++)
                p_devices[i] = devices[i].Handle;
            GCHandle[] handles = new GCHandle[binaries.LongLength];
            IntPtr[] p_binaries = new IntPtr[binaries.LongLength];
            SizeT[] lengths = new SizeT[binaries.LongLength];
            for (long i = 0; i < binaries.LongLength; i++)
            {
                handles[i] = GCHandle.Alloc(binaries[i], GCHandleType.Pinned);
                p_binaries[i] = handles[i].AddrOfPinnedObject();
                lengths[i] = binaries[i].LongLength;
            }
            ErrorCode[] binary_status = new ErrorCode[binaries.LongLength];
            GCHandle binary_status_handle = GCHandle.Alloc(binary_status, GCHandleType.Pinned);
            Handle = clCreateProgramWithBinary(context.Handle, (uint)devices.LongLength, p_devices, lengths, p_binaries, binary_status_handle.AddrOfPinnedObject(), ref res);
            for (long i = 0; i < binary_status.LongLength; i++)
                if (binary_status[i] != ErrorCode.Success)
                    throw new OpenCLException(binary_status[i]);
            if (res != ErrorCode.Success)
                throw new OpenCLException(res);
            for (long i = 0; i < binaries.LongLength; i++)
                handles[i].Free();
            binary_status_handle.Free();
        }

        static Program CreateWithBuildInKernels(Context context, Device[] devices, string kernel_names)
        {
            IntPtr[] p_devices = new IntPtr[devices.LongLength];
            for (long i = 0; i < p_devices.LongLength; i++)
                p_devices[i] = devices[i].Handle;
            ErrorCode res = ErrorCode.Success;
            IntPtr handle = clCreateProgramWithBuildInKernels(context.Handle, (uint)devices.LongLength, p_devices, kernel_names, ref res);
            if (res != ErrorCode.Success)
                throw new OpenCLException(res);
            return new Program(handle);
        }

        public void Build(string options, params Device[] devices)
        {
            IntPtr[] p_devices = new IntPtr[devices.LongLength];
            for (long i = 0; i < devices.LongLength; i++)
                p_devices[i] = devices[i].Handle;
            ErrorCode res = clBuildProgram(Handle, (uint)devices.LongLength, p_devices, options, null, null);
            if (res != ErrorCode.Success)
                throw new OpenCLException(res);
        }

        public void Compile(string options, params Device[] devices)
        {
            throw new NotImplementedException();
        }

        public void Link(string options, params Device[] devices)
        {
            throw new NotImplementedException();
        }

        [Obsolete("Renamed as of OpenCL 1.2 -> use UnloadPlatformCompiler() instead")]
        public static void UnloadCompiler()
        {
            ErrorCode res = clUnloadCompiler();
            if (res != ErrorCode.Success)
                throw new OpenCLException(res);
        }

        public static void UnloadPlatformCompiler()
        {
            ErrorCode res = clUnloadPlatformCompiler();
            if (res != ErrorCode.Success)
                throw new OpenCLException(res);
        }

        public object Clone()
        {
            ErrorCode res = clRetainProgram(Handle);
            if (res != ErrorCode.Success)
                throw new OpenCLException(res);
            return new Program(Handle);
        }

        public void Dispose()
        {
            ErrorCode res = clReleaseProgram(Handle);
            if (res != ErrorCode.Success)
                throw new OpenCLException(res);
            Handle = IntPtr.Zero;
        }
    }
}
